[JAVA 기술면접] DB Connection Pool — 사용 이유와 적정 크기

NOTE

질문: DB Connection이 무제한으로 늘어나는 것을 막으려고 Connection Pool을 쓰는데, 왜 쓰는가? 그리고 적절한 풀 크기는 어떤 기준으로 정하는가? 핵심은 두 가지다. ① 풀의 가치는 “개수 제한”뿐 아니라 연결 생성 비용 제거와 DB 보호다. ② 풀 크기는 “클수록 좋다”가 아니라 DB가 실제로 동시에 처리할 수 있는 양과 쿼리 점유 시간으로 정한다.


Q1. 왜 Connection Pool을 쓰는가?

1) 연결 생성 비용이 크다

요청마다 연결을 새로 만들면 매번 다음을 치른다.

TCP 3-way handshake → (TLS 핸드셰이크) → DB 인증/세션 초기화 → 쿼리 → 연결 종료

쿼리 자체는 1ms인데 연결 수립에 수 ms~수십 ms가 걸리기도 한다. 풀은 미리 만들어 둔 연결을 빌려 쓰고 반납하므로 이 비용을 없앤다.

2) DB 서버를 보호한다

  • DB는 연결마다 자원을 쓴다. MySQL은 연결당 스레드, PostgreSQL은 연결당 프로세스를 두고 메모리(세션 버퍼 등)를 점유한다.
  • DB에는 max_connections 상한이 있다(MySQL 기본 151). 초과하면 Too many connections로 신규 연결이 거절된다.
  • 풀이 없으면 트래픽 급증 시 연결 수가 폭증해 DB가 먼저 무너진다. 풀은 상한을 두고 초과분은 대기시켜 부하를 평탄화한다(대기열 역할).

3) 자원 관리를 한곳에서 한다

  • 연결 재사용, 유효성 검사(죽은 연결 제거), 수명 관리, 누수 감지, 모니터링 지표를 풀이 제공한다.
  • 애플리케이션은 getConnection() / close()(= 풀에 반납)만 하면 된다. close()는 실제 연결을 끊지 않고 풀로 되돌린다.

정리 표

풀 없음풀 사용
요청당 연결매번 생성·종료빌려 쓰고 반납
지연연결 수립 비용 포함즉시 사용
DB 연결 수요청 수에 비례해 폭증 가능상한 고정
장애 시DB 다운으로 확산대기/타임아웃으로 완충

Q2. 적절한 풀 크기는 어떤 기준으로 잡는가?

먼저 알아야 할 사실: “크다고 빠르지 않다”

DB는 CPU 코어와 디스크의 한계가 있어 동시에 실제로 실행할 수 있는 쿼리 수가 제한적이다. 연결이 그보다 많아지면 쿼리가 빨라지는 것이 아니라 컨텍스트 스위칭·락 경합·메모리 압박만 늘어 오히려 느려진다. 풀이 크면 스레드는 DB 앞에서 병목을 만나고, 풀이 작으면 애플리케이션 앞에서 대기한다. 어느 쪽이든 대기는 있고, 큰 풀은 그 대기를 DB 내부로 옮길 뿐이다.

기준 1) 경험식 (HikariCP 위키의 출발점)

connections = (DB 서버의 CPU 코어 수 × 2) + 유효 디스크 스핀들 수
  • 예: 4코어 DB 서버 → 약 9~10개. 생각보다 작다.
  • 이 식은 출발점이다. SSD·캐시 적중률이 높으면 디스크 대기가 줄어 더 작아도 충분하고, I/O 대기가 길면 더 필요할 수 있다.
  • 기준은 애플리케이션 서버가 아니라 DB 서버의 코어다.

기준 2) 리틀의 법칙 (처리량과 점유 시간)

필요한 동시 연결 수 ≈ 초당 요청 수(TPS) × 연결 평균 점유 시간(초)
  • 예: 500 TPS, 요청당 DB 연결을 평균 20ms(0.02초) 점유 → 500 × 0.02 = 10개.
  • 점유 시간이 100ms로 늘면 같은 TPS에 50개가 필요하다. 즉 풀을 늘리기 전에 점유 시간을 줄이는 것(쿼리 튜닝, 트랜잭션 단축)이 먼저다.
  • 여유율(피크, 지연 편차)을 곱해 보정한다.

기준 3) 전체 인스턴스 합이 DB 한도 안에 있어야 한다

(앱 인스턴스 수) × (인스턴스당 maximumPoolSize) + (배치·관리·모니터링 등 다른 접속)  ≤  DB max_connections (여유 포함)
  • 서버를 스케일 아웃하면 풀 크기를 그대로 둬도 DB 총 연결 수가 늘어난다. 오토스케일링 최대 대수 기준으로 계산한다.
  • 한 DB를 여러 서비스가 공유하면 서비스별 할당량을 정한다.
  • 연결 수가 너무 많아지면 DB 앞단에 프록시/풀러(ProxySQL, PgBouncer, RDS Proxy)를 둔다.

기준 4) 스레드 풀과의 균형

  • 요청 스레드(Tomcat maxThreads 200)가 모두 DB를 쓰는 것은 아니다. 스레드 200개에 풀 10개여도 DB를 쓰는 시간 비율이 낮으면 충분할 수 있다.
  • 반대로 트랜잭션 안에서 외부 API 호출·파일 처리·sleep 을 하면 연결을 쥔 채 대기해 풀이 마른다. 트랜잭션 범위를 DB 작업만으로 좁힌다.
  • 비동기 작업·배치 스레드가 같은 풀을 공유하면 서로 굶길 수 있으므로 풀을 분리(읽기/쓰기, 온라인/배치)하기도 한다.

기준 5) 측정하고 조정한다

정답은 부하 테스트로 정한다.

지표의미신호
Active 연결 수사용 중인 연결상시 최대치에 붙어 있으면 부족하거나 점유 시간이 김
Pending(대기 스레드 수)연결을 기다리는 스레드0이 아니면 풀 대기 발생
Connection acquire 시간풀에서 연결을 얻기까지길면 풀 고갈
Connection usage 시간연결을 쥐고 있는 시간길면 쿼리/트랜잭션 문제
DB 쪽 활성 세션 수, CPU, 락 대기DB가 감당하는지풀을 늘렸더니 DB CPU만 오르면 과도

조정 순서 권장: 느린 쿼리/트랜잭션 범위 개선 → 그래도 대기가 있으면 풀 증가 → DB 한도 근접하면 캐시·읽기 복제·프록시 도입.


HikariCP 주요 설정 (Spring Boot 기본 풀)

설정기본값설명과 권장
maximumPoolSize10풀의 최대 연결 수. 위 기준으로 산정
minimumIdlemaximumPoolSize와 동일유휴 최소 연결. 고정 크기 풀을 권장(수요 급증 시 연결 생성 지연 방지)
connectionTimeout30초풀에서 연결을 얻기 위해 기다리는 최대 시간. 초과하면 예외. 사용자 응답 시간 관점에서 더 짧게(예: 3~5초) 두는 경우가 많음
maxLifetime30분연결의 최대 수명. DB/방화벽의 연결 종료 시간(wait_timeout 등)보다 수 초~수 분 짧게 설정해 끊긴 연결 사용을 방지
idleTimeout10분유휴 연결 정리 시간(minimumIdle < max일 때만 의미)
keepaliveTime0(비활성)유휴 연결에 주기적으로 핑을 보내 중간 장비의 끊김 방지
leakDetectionThreshold0(비활성)이 시간 이상 반납되지 않으면 누수 의심 로그(스택트레이스)
validationTimeout5초연결 유효성 검사 시간 제한
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 20          # 고정 크기
      connection-timeout: 3000  # ms
      max-lifetime: 1740000     # 29분 (DB wait_timeout 30분보다 짧게)
      leak-detection-threshold: 20000

풀 고갈(Pool Exhaustion) 사례와 대응

원인증상대응
연결 누수 (close() 누락, 예외 시 반납 안 됨)Active가 계속 최대, 점점 응답 불가try-with-resources, Spring 트랜잭션 사용, leakDetectionThreshold로 위치 추적
트랜잭션 안에서 외부 호출/긴 처리연결 점유 시간 증가트랜잭션 범위 축소, 외부 호출은 밖으로
느린 쿼리·락 대기점유 시간 증가, 연쇄 대기인덱스/쿼리 개선, 락 범위 축소, 쿼리 타임아웃
OSIV(Open-In-View) 활성컨트롤러·뷰 렌더링까지 연결 유지OSIV 비활성화, DTO로 변환 후 반환
트래픽 급증Pending 급증캐시, 비동기 큐, Rate Limit, 서킷 브레이커로 DB 앞에서 완충
풀 크기 과대 + 인스턴스 증가DB Too many connections총합 계산, 프록시 도입

풀이 마르면 Connection is not available, request timed out after 30000ms 같은 예외가 난다. 원인은 풀 크기 부족이 아니라 “누가 연결을 오래 쥐고 있나”인 경우가 대부분이다.


꼬리 질문 대비

질문답변 요지
풀의 close()는 무엇을 하나?물리 연결을 끊지 않고 풀에 반납한다(프록시 래퍼가 가로챔).
풀 크기를 무작정 늘리면?DB 컨텍스트 스위칭·메모리·락 경합 증가로 오히려 느려지고, 총 연결 수가 max_connections에 닿아 장애가 날 수 있다.
풀 크기가 너무 작으면?요청이 풀 대기열에서 기다려 지연이 늘고, connectionTimeout을 넘으면 실패한다.
읽기/쓰기 분리 환경에서는?보통 Primary용·Replica용 풀을 따로 두고 읽기 전용 트랜잭션은 Replica 풀로 라우팅(AbstractRoutingDataSource). 각각 별도로 크기를 산정.
왜 HikariCP가 빠른가?바이트코드 수준 최적화, 락 경합을 줄인 자체 컬렉션(ConcurrentBag), 불필요한 검사 최소화로 오버헤드가 낮다. Spring Boot 2.0부터 기본 풀.
커넥션 풀과 스레드 풀의 차이?스레드 풀: 요청을 처리할 실행 단위 재사용. 커넥션 풀: DB 연결 재사용. 서로 다른 자원이지만 한쪽 병목이 다른 쪽에 영향을 준다(연결 대기 중인 스레드는 놀고 있다).
풀 크기를 어떻게 검증하나?실제 트래픽 패턴으로 부하 테스트하며 Active/Pending/Acquire 시간과 DB CPU·활성 세션을 본다. 한 번에 한 값만 바꿔 비교.
서버리스/오토스케일 환경의 주의점?인스턴스가 급증하면 총 연결 수가 폭증한다. 인스턴스당 풀을 작게, 또는 외부 프록시(RDS Proxy 등)로 연결을 다중화한다.
maxLifetime을 왜 DB 타임아웃보다 짧게?DB·방화벽이 먼저 연결을 끊으면 풀이 끊긴 연결을 내줘 Communications link failure 같은 오류가 난다. 풀이 먼저 갱신하도록 한다.

한 줄 요약

  • 왜: 연결 생성 비용을 없애고, DB의 연결 상한과 자원을 지키며, 초과 부하를 대기로 완충하기 위해.
  • 크기: DB 코어×2+스핀들을 출발점으로, TPS × 점유 시간으로 검증하고, 인스턴스 수 × 풀 크기 ≤ DB 한도를 확인한 뒤, 측정으로 조정한다. 늘리기 전에 점유 시간부터 줄인다.

관련 문서